#!/usr/bin/perl
#
# Heuristic routines for checking whether code conforms to the SCIP
# coding style guideline. We check the following:
#
# - List lines with trailing space
#   [checktrailspace, default: true]
#
# - List function parameter lines that are not indented as specified
#   by the SCIP coding style.
#   [checkindent, default: true]
#
# - List lines where tabs occur.
#   [checktabs, default: false, -t option, "-o" to only output tabs]
#
# - List lines in which the braces seem to be at the wrong place
#   [checkbraces, default: false, -b option].
#
# - List occurences of printf's.
#   [checkothers, default: true]
#
# - List lines that contain SCIP_CALL_ABORT (except checkStage()).
#   [checkothers, default: true]
#
# - List SCIPdebugMessage() calls that are not terminated with "\n".
#   [checkdebug, default: true, -n option]
#
# - List doxygen documentation within function body (this would lead
#   to badly generated doxygen documentation).
#   [checkdocinbody, default: true]
#
# - List differences between function name and the one specified in
#   checkStage (only for scip.c).
#   [checkstage, default: false, option: -s]
#
# - Check whether two comments occur in one line; this sometimes indicates
#   bad style
#   [checkcomments, default: false]
#
# - Check for empty lines after "{" in the next line or before "}"
#   [checkemptylines, default: true]
#
# - Check for spaces around operators; non-strict setting allows operators with numbers like "a+1"
#   [checkoperspaces, default: false]
#
# - Check for the presence of "/**>", which is likely to be a typo.
#
# Note the this will not give a complete check. The checks are also
# heuristic in order to avoid to be flooded with warnings. To
# guarantee correct indentation of the code, one should use the
# Emacs-functions instead. This script is just thought to be
# complementary to the existing methods.
#
# Known issues: Does not check indentation of enums or unions correctly.
my $narg = @ARGV;

if ( $narg < 1 || $ARGV[0] eq "-h" )
{
   printf("usage: <.> [-h] [-t] [-b] [-n] [-o] [-s] [-a] <files>\n");
   printf("-t: output if tabs are present;\n");
   printf("-b: output possible positions of badly placed braces;\n");
   printf("-n: warn about missing newlines in debug messages;\n");
   printf("-o: other warnings: SCIP_CALL_ABORT and printfs;\n");
   printf("-s: spaces around operators;\n");
   printf("-a: all checks on.\n");
   exit(1);
}

# determine which part to check
my $checktabs = 0;       # check whether tabs are present
my $checkbraces = 0;     # check whether braces are badly placed
my $checkdebug = 0;      # check whether debug messages contain a newline at end
my $checkothers = 0;     # check for SCIP_CALL_ABORT and printfs
my $checkcomments = 0;   # check for two comments in a line
my $checkoperspaces = 0; # check for spaces around operators
my $checkoperstrict = 0; # check strict rules for operators

my $checktrailspace = 1; # check for trailing space
my $checkindent = 1;     # check for correct indentation
my $checksepdoclines = 1;# check for separate documentation lines
my $checktabsparam = 1;  # check whether tabs in parameter lists are present
my $checkstage = 1;      # check whether names of functions for checkstage are correct in scip.c
my $checkdocinbody = 1;  # check whether doxygen documentation appears within code
my $checkemptylines = 1; # check for empty lines after "{" in the next line or before "}"
my $checkscipcall = 1;   # check whether spaces are correct for SCIP_CALL(_ ... _)

my $nfiles = 0;

# parse command line arguments (including switches and files)
for (my $i = 0; $i < $narg; ++$i)
{
   # check for tab option
   if ( $ARGV[$i] eq "-t" )
   {
      $checktabs = 1;
      next;
   }

   # check for tab option
   if ( $ARGV[$i] eq "-b" )
   {
      $checkbraces = 1;
      next;
   }

   # check for debug message
   if ( $ARGV[$i] eq "-n" )
   {
      $checkdebug = 1;
      next;
   }

   # check for checkothers
   if ( $ARGV[$i] eq "-o" )
   {
      $checkothers = 1;
      next;
   }

   # check for checkoperspaces
   if ( $ARGV[$i] eq "-s" )
   {
      $checkoperspaces = 1;
      next;
   }

   # check for all
   if ( $ARGV[$i] eq "-a" )
   {
      $checkothers = 1;
      $checktabs = 1;
      $checkbraces = 1;
      $checkdebug = 1;
      $checkoperspaces = 1;
      next;
   }

   # try to open file
   open FILE, $ARGV[$i] or die $!;

   # pass through file
   if ( $nfiles > 0 )
   {
      printf("\n\n");
   }
   printf("file: %s\n", $ARGV[$i]);
   ++$nfiles;

   my $line = 0;
   my $incomment = 0;
   my $indocumentation = 0;
   my $depth = 0;
   my $afterdocumentation = 0;
   my $functionname = "";
   my $documentationpos = -1;
   my $foundexternc = 0;
   my $inenumstruct = 0;
   my $emptyline = 0;
   my $lastemptyline = -1;
   my $openbraceprev = 0;

   while(<FILE>)
   {
      chomp;
      ++$line;
      my $str = $_;
      $documentationpos = -1;

      # check trailing space
      if ( $checktrailspace == 1 && $str =~ /\s$/ )
      {
         printf("%d: trailing space\n", $line);
      }

      # ----------------------------------------
      # check for empty lines in functions
      if ( $depth > $foundexternc && $checkemptylines == 1 )
      {
         # check for empty lines
         if ( length($str) == 0 )
         {
            ++$emptyline;
            $lastemptyline = $line;

            # check for empty lines following an opening block
            if ( $openbraceprev > 0 )
            {
               # directly warn within a function
               if ( $depth > $foundexternc + 1 )
               {
                  printf("%d: empty line following an opening block.\n", $line);
               }
               else
               {
                  # we do not want to warn if the first line after the
                  # empty line is a SCIP_CALL at the beginning of a
                  # function. Thus, we read the next line and check.
                  $str = <FILE>;
                  chomp($str);
                  ++$line;

                  # do not warn if we directly call another function
                  if ( $str !~ /SCIP\_CALL\(/ )
                  {
                     printf("%d: empty line following an opening block.\n", $line);
                  }
                  else
                  {
                     # we now have to update the empty line status
                     if ( length($str) == 0 )
                     {
                        ++$emptyline;
                        $lastemptyline = $line;
                     }
                     else
                     {
                        $emptyline = 0;
                     }
                  }
               }
            }
         }
         else
         {
            if ( $emptyline >= 2 )
            {
               printf("%d: %d consecutive empty lines in code.\n", $line - 1, $emptyline);
            }
            $emptyline = 0;
         }
      }
      $openbraceprev = 0;

      # ----------------------------------------
      # check for SCIPdebugMessage without "\n"
      if ( $checkdebug == 1 && $str =~ /SCIPdebugMessage/ )
      {
         my $s = $str;

         # remove "SCIP_LONGINT_FORMAT"
         $s =~ s/\"SCIP_LONGINT_FORMAT\"//g;

         my $debpos = index($s, "SCIPdebugMessage");
         my $enddebpos = index($s, "\",", $debpos + 18);
         if ( $enddebpos >= 0 )
         {
            if ( substr($s, $enddebpos-2, 2) ne "\\n" )
            {
               printf("%d: possible SCIPdebugMessage() without finializing '\\n':\n%s\n\n", $line, $_);
            }
         }
      }

      # ----------------------------------------
      # check for "SCIP_CALL(...)" without whitespace
      if ( $checkscipcall == 1 )
      {
         if ( $str =~ /SCIP_CALL\([^\s]/ )
         {
            printf("%d: found SCIP_CALL( without following whitespace:\n%s\n\n", $line, $_);
         }
         elsif ( $str =~ /SCIP_CALL\(.*[^\s]\);/ )
         {
            printf("%d: found SCIP_CALL( without final whitespace:\n%s\n\n", $line, $_);
         }
      }

      # ----------------------------------------
      # check checkStage function (only for scip_*.c)
      if ( $checkstage == 1 && $str =~ /checkStage\(scip/ )
      {
         my @str1 = split(/checkStage/, $str);
         my @str2 = split(/,/, $str1[1]);
         my $s = $str2[1];

         # strip quotes
         $s =~ s/\"//g;
         # strip spaces
         $s =~ s/\s//g;

         if ( $s ne $functionname )
         {
            printf("%d: possible wrong checkstage name: <%s> <%s>\n", $line, $s, $functionname);
         }
      }

      # ---------------------------------------------------------
      # handle strings (must be executed after the functions above that need strings)

      # remove everything within a string
      if ( $str =~ /\".*\"/ && $str !~ /(extern \"C\" \{)/ )
      {
         # remove minimal occurence of string
         $str =~ s/\".*?\"//g;
      }

      # ---------------------------------------------------------
      # handle comments

      # check for the presence of "/**>"
      if ( $str =~ /\/\*\*>/ )
      {
         printf("%d: found \"/**>\", likely to be an error.\n%s\n", $line, $_);
      }

      # make sure that no comment ends and afterwards starts on the same line
      if ( $checkcomments == 1 && $str =~ /\*\/(.*)\/\*/ )
      {
         if ( $str !~ /\*lint/ )
         {
            # simply warn, but continue (parsing might not be correct, however)
            printf("%d: line contains two comments - merge into one?\n%s\n\n", $line, $_);
         }
      }

      # if we are in a comment that started in previous lines, we search for the end of the comment
      if ( $incomment == 1 || $indocumentation == 1 )
      {
         # if we want to check separate documentation lines
         if ( $checksepdoclines == 1 )
         {
            # if the line does not contain the start of a comment and there is nothing before
            if ( $documentationpos < 0 && $inenumstruct == 0 && $depth == 0 )
            {
               if ( $_ =~ /[\w\*]/ )
               {
                  my $startpos = $-[0];
                  if ( $startpos >= 45 )
                  {
                     if ( $_ !~ /\*/ )
                     {
                        printf("%d: line contains separate comment without '*'\n%s\n\n", $line, $_);
                     }
                     elsif ( $startpos != 46 )
                     {
                        printf("%d: line contains separate comment that does not start at position 45:\n%s\n\n", $line, $_);
                     }
                  }
               }
            }
         }

         $afterdocumentation = 0;
         if ( $str =~ /\*\// )
         {
            # remove comment from current line
            $str =~ s/(.*)\*\///g;

            if ( $indocumentation == 1 )
            {
               $afterdocumentation = 1;
            }
            $incomment = 0;
            $indocumentation = 0;
         }
         else
         {
            # skip line (comment continues)
            next;
         }
      }
      else
      {
         # if a comment starts or ends in current line
         if ( $str =~ /\/\*/ || $str =~ /\*\// )
         {
            # determine position of documentation string (-1 otherwise); needed for indentation checking
            if ( $checkindent == 1 && index($str, "\#define") < 0 && index($str, "typedef") < 0 )
            {
               $documentationpos = index($_, "/**<");
            }

            # check whether we have a doxygen documentation within the code
            if ( $checkdocinbody == 1 && $str =~ /\/\*\*/ && ($depth - $foundexternc - $inenumstruct) > 0 )
            {
               if ( $str !~ /\/\*\*(\s*)\@/ && $str !~ /\/\*\*\*\*/ && $str !~ /\[Snippet/ )
               {
                  printf("%d: possible doxygen documentation within function (depth: %d, enumstruct: %d):\n%s\n\n", $line, $depth, $inenumstruct, $_);
               }
            }

            # handle one-line documentation
            if ( $str =~ /\/\*\*(.*)\*\// )
            {
               # remove documentation comments that are within the current line
               $str =~ s/\/\*\*(.*)\*\///g;
               $afterdocumentation = 1;
            }
            else
            {
               # handle one-line comments
               if ( $str =~ /\/\*(.*)\*\// )
               {
                  # remove comments that are within the current line
                  $str =~ s/\/\*(.*)\*\///g;
               }
            }

            # check for comment without start that ends
            if ( $str =~ /\*\// )
            {
               printf("%d: there is not comment to end here:\n%s\n\n", $line, $_);
            }

            # check whether documentation starts
            if ( $str =~ /\/\*\*/ )
            {
               $indocumentation = 1;
               $incomment = 1;
               $afterdocumentation = 0;
               next;
            }

            # check whether comment starts
            if ( $str =~ /\/\*/ )
            {
               $incomment = 1;
               $afterdocumentation = 0;
               next;
            }
         }
      }

      # -----------------------------------------------------------------------------
      # calculate depth

      # determine whether we are in an enum or struct (assume it is at the beginning of the line)
      if ( $str =~ /^enum/ || $str =~ /^struct/ || $str =~ /^typedef struct/ || $str =~ /^typedef enum/ || $str =~ /\s*union/ )
      {
         ++($inenumstruct);
      }

      # check for opening blocks
      if ( $str =~ /\{/ && $str !~ /'\{'/ && $incomment == 0 )
      {
         # mark beginning of block if block is not closing on same line
         if ( $str !~ /\{(.*)\}/ )
         {
            $openbraceprev = 1;
         }

         if ( $str =~ /\@\{/ )
         {
            printf("%d: '\@{' within non-comment line:\n%s\n\n", $line, $_);
         }
         else
         {
            if ( $str =~ /(extern \"C\" \{)/ )
            {
               $foundexternc = 1;
            }

            # loop through all occurences in current row
            my $pos = -1;
            do
            {
               $pos = index($str, "{", $pos + 1);
               if ( $pos >= 0 )
               {
                  ++$depth;
               }
            }
            while ( $pos >= 0 );
         }
      }

      # check for closing blocks
      if ( $str =~ /\}/ && $str !~ /'\}'/ && $incomment == 0 )
      {
         # check for empty lines preceeding a closeing block
         if ( $lastemptyline == $line - 1 && $depth > $foundexternc && $checkemptylines == 1 && $str !~ /\{(.*)\}/ )
         {
            printf("%d: empty line preceeding closing block.\n", $line);
         }

         --($inenumstruct);
         if ( $inenumstruct < 0 )
         {
            $inenumstruct = 0;
         }

         if ( $str =~ /\@\}/ )
         {
            printf("%d: '\@}' within non-comment line:\n%s\n\n", $line, $_);
         }
         else
         {
            # loop through all occurences in current row
            my $pos = -1;
            do
            {
               $pos = index($str, "}", $pos + 1);
               if ( $pos >= 0 )
               {
                  --$depth;
                  if ( $depth + $foundexternc < 0 )
                  {
                     printf("Warning: brace count wrong!\n");
                     $depth = 0;
                  }
               }
            }
            while ( $pos >= 0 );
         }
      }

      # printf("%d: %d (%d %d): %s\n", $line, $depth, $foundexternc, $inenumstruct, $str);

      # -----------------------------------------------------------
      # try to determine function names (needed for checking stage)
      if ( $checkstage == 1 )
      {
         # try to determine function name
         if ( $afterdocumentation == 1 )
         {
            # check for parenthesis
            if ( $depth == 0 && $str =~ /\S\(/ && $str !~ /\)\)/ )
            {
               my @mystr = split(/\(/, $str);
               my @s = split(/\s+/, $mystr[0]);

               # guess function name
               $functionname = $s[$#s];
               $afterdocumentation = 0;
            }
         }
      }

      # -----------------------------------------------------------
      # check for tabs
      if ( $checktabs )
      {
         if ( $str =~ /\t/ )
         {
            # replace '\t' by "********"
            my $s = $str;
            $s =~ s/\t/\*\*\*\*\*\*\*\*/g;
            # output modified line
            printf("%d: found tabs:\n%s\n\n", $line, $s);
         }
      }

      # check tabs in parameter lists
      if ( $checktabsparam == 1 && $documentationpos >= 0 && $inenumstruct == 0 )
      {
         if ( $str =~ /\t/ )
         {
            # replace '\t' by "********"
            my $s = $str;
            $s =~ s/\t/\*\*\*\*\*\*\*\*/g;
            # output modified line
            printf("%d: found tabs:\n%s\n\n", $line, $s);
         }
      }

      # ----------------------------------------
      # check braces
      if ( $checkbraces && $incomment == 0 )
      {
         # ignore 'extern "C"{'
         my $s = $str;
         $s =~ s/(extern \"C\" \{)//g;

         # check for braces
         if ( $s =~ /\{/ || $s =~ /\}/ )
         {
            # strip spaces:
            $s =~ s/\s+//g;

            # strip trailing ';';
            $s =~ s/;$//g;

            if ( length($s) > 1 )
            {
               printf("%d: brace: %s\n\n", $line, $_);
            }
         }
      }

      # ------------------------------------------------------------
      # check indentation of function parameters closing parenthesis
      if ( $checkindent == 1 && $documentationpos <= 0 && $depth == 0 && $inenumstruct == 0 )
      {
         if ( ! (($str =~ /SCIP_DECL_/) || ($str =~ /DECL_/) || ($str =~ /TCLIQUE_/) || ($str =~ /SORTTPL\_/) || ($str =~ /define/) ) )
         {
            # check for ")"
            my $pos = index($str, ")");

            if ( $pos >= 0 && $pos != 3 )
            {
               printf("%d: wrong indentation of function closing parenthesis: <%s>\n", $line, $str);
            }
         }
      }

      # ----------------------------------------
      # check indentation of function parameters
      if ( $checkindent == 1 && $documentationpos >= 0 && $inenumstruct == 0 )
      {
         if ( ! ($str =~ /SORTTPL\_/) )
         {
            # make sure that there is a space before "/**<" (needed below)
            my $s = $_;
            $s =~ s/\/\*\*</ \/\*\*</g;

            # split into components
            @array = split(/\s+/, $s);

            # check for first component
            my $pos = index($_, $array[1]);

            # first component should always start at column 3
            if ( $pos != 3 )
            {
               printf("%s\n", $_);
               for (my $j = 0; $j < $pos; ++$j)
               {
                  printf(" ");
               }
               printf("^  (pos: %d, line: %d)\n\n", $pos, $line);
               next;
            }

            # try to find variable name = last component before comment (may not exist)
            my $ind = 1;
            while ( $ind < $#array && $array[$ind] !~ /\/\*\*</ )
            {
               ++$ind;
               $pos = index($_, $array[$ind], $pos+1);
            }

            # check whether variable name is aligned
            if ( $ind >= 2 )
            {
               $pos = index($_, $array[$ind-1], 22);
               # make sure previous names are not too long
               my $l = 3;
               for (my $k = 1; $k < $ind-1; ++$k)
               {
                  $l = $l + length($array[$k]) + 1;
               }
               if ( $l == $pos )
               {
                  next;
               }

               if ( $pos != 25 && $pos > 3 )
               {
                  printf("%d %s %s\n", $ind, $array[1], $array[2]);
                  printf("%s\n", $_);
                  printf("%-5d: pos: %2d", $line, $pos);
                  for (my $j = 14; $j < $pos; ++$j)
                  {
                     printf(" ");
                  }
                  printf("^\n\n");
                  next;
               }
            }

            # check for comment
            if ( $documentationpos != 45 )
            {
               # skip too long variable types
               if ( 25 + length($array[$ind-1]) <= 42 )
               {
                  printf("%s\n", $_);
                  printf("%-5d: pos: %2d", $line, $documentationpos);
                  for (my $j = 14; $j < $documentationpos; ++$j)
                  {
                     printf(" ");
                  }
                  printf("^\n\n");
               }
            }
         }
      }

      # ----------------------------------------
      # check spaces around operators
      if ( $checkoperspaces == 1 )
      {
         if ( $checkoperstrict == 1 )
         {
            if ( $str =~ /\w[\+\*\%\-]\w/ )
            {
               printf("%d: operator without surrounding spaces: %s\n\n", $line, $_);
            }
         }
         else
         {
            if ( $str =~ /[a-zA-Z][\+\*\%\-][a-zA-Z]/ )
            {
               printf("%d: operator without surrounding spaces: %s\n\n", $line, $_);
            }
         }
      }

      # ----------------------------------------
      # check other stuff
      if ( $checkothers )
      {
         # check for printfs
         if ( $str =~ / printf\(/ )
         {
            printf("%d: printf found: %s\n\n", $line, $_);
         }

         # check for SCIP_CALL_ABORT
         if ( $str =~ /SCIP\_CALL\_ABORT\(/ )
         {
            # SCIP_CALL_ABORT allowed for checkStage in scip.c
            if ( ! ($_ =~ /SCIP\_CALL\_ABORT\( checkStage/) )
            {
               printf("%d: SCIP_CALL_ABORT found: %s\n\n", $line, $_);
            }
         }
      }
   }
}
